package domein;

import java.util.ArrayList;

import aima.core.environment.map.AdaptableHeuristicFunction;
import domein.ExtendableMap;
import aima.core.environment.map.MapAgent;
import aima.core.environment.map.MapEnvironment;
import aima.core.environment.map.Scenario;
import aima.core.util.datastructure.Point2D;
import domein.AbstractMapAgentController;
import domein.ExtendedMapAgentView;
import domein.MapAgentFrame;
import domein.SearchFactory;
import aima.gui.framework.AgentAppController;
import aima.gui.framework.AgentAppEnvironmentView;
import aima.gui.framework.AgentAppFrame;
import aima.gui.framework.MessageLogger;
import aima.gui.framework.SimpleAgentApp;

/**
 * Demo example of a route finding agent application with GUI. The main method
 * starts a map agent frame and supports runtime experiments. This
 * implementation is based on the {@link aima.core.environment.map.MapAgent} and
 * the {@link aima.core.environment.map.MapEnvironment}. It can be used as a
 * code template for creating new applications with different specialized kinds
 * of agents and environments.
 * 
 * @author Ruediger Lunde
 */
public class RouteFindingAgentApp extends SimpleAgentApp {

	/** Creates a <code>MapAgentView</code>. */
	public AgentAppEnvironmentView createEnvironmentView() {
		return new ExtendedMapAgentView();
	}

	/** Creates and configures a <code>RouteFindingAgentFrame</code>. */
	@Override
	public AgentAppFrame createFrame() {
		return new RouteFindingAgentFrame();
	}

	/** Creates a <code>RouteFindingAgentController</code>. */
	@Override
	public AgentAppController createController() {
		return new RouteFindingAgentController();
	}

	// //////////////////////////////////////////////////////////
	// local classes

	/** Frame for a graphical route finding agent application. */
	protected static class RouteFindingAgentFrame extends MapAgentFrame {
		private static final long serialVersionUID = 1L;

		/** Creates a new frame. */
		public RouteFindingAgentFrame() {
			setTitle("RFA - the Route Finding Agent");
			setSelectorItems(SCENARIO_SEL, new String[] { "Random Map",
					"Hardcoded Map" }, 0);
			setSelectorItems(SEARCH_MODE_SEL, SearchFactory.getInstance()
					.getSearchModeNames(), 1); // change the default!
			setSelectorItems(HEURISTIC_SEL, new String[] { "No Heuristic",
					"Manhattan Distance", "Euclidean Distance", "Diagonal Distance"}, 0);
		}

		/**
		 * Changes the destination selector items depending on the scenario
		 * selection if necessary, and calls the super class implementation
		 * afterwards.
		 */
		@Override
		protected void selectionChanged(String changedSelector) {
			super.selectionChanged(changedSelector);
		}
	}

	/** Controller for a graphical route finding agent application. */
	protected static class RouteFindingAgentController extends
			AbstractMapAgentController {
		private ExtendableMap map = null;
		private MapEnvironment env = null;
		private CustomMap cMap = null;

		/**
		 * Configures a scenario and a list of destinations. Note that for route
		 * finding problems, the size of the list needs to be 1.
		 */
		@Override
		protected void selectScenarioAndDest(int scenarioIdx, int destIdx) {
			if (map == null)
				map = new ExtendableMap();
			env = new MapEnvironment(map);

			String agentLoc = null;

			switch (scenarioIdx) {
			case 0:
				if (cMap == null)
					cMap = new CustomMap();
				cMap.initialize(map);
				agentLoc = "" + cMap.getStartPosition();
				break;
			case 1:
				SimpleMaze.initialize(map);
				agentLoc = "" + SimpleMaze.getStartPosition();
				break;
			}

			scenario = new Scenario(env, map, agentLoc);
			destinations = new ArrayList<String>();

			switch (scenarioIdx) {
			case 0:
				destinations.add("" + cMap.getGoalPosition());
				break;
			case 1:
				destinations.add("" + SimpleMaze.getGoalPosition());
				break;
			}
		}

		/**
		 * Prepares the view for the previously specified scenario and
		 * destinations.
		 */
		@Override
		protected void prepareView() {
			ExtendedMapAgentView mEnv = (ExtendedMapAgentView) frame
					.getEnvView();
			mEnv.setData(scenario, destinations, null);
			mEnv.setEnvironment(scenario.getEnv());
		}

		/**
		 * Returns the trivial zero function or a simple heuristic which is
		 * based on straight-line distance computation.
		 */
		@Override
		protected AdaptableHeuristicFunction createHeuristic(int heuIdx) {
			AdaptableHeuristicFunction ahf = null;
			switch (heuIdx) {
			case 0:
				ahf = new No_Heuristic();
				break;
			case 1:
				ahf = new Manhattan_Distance();
				break;
			case 2:
				ahf = new Euclidean_Distance();
				break;
			default:
				ahf = new Diagonal_Distance();
			}
			return ahf.adaptToGoal(destinations.get(0), scenario.getAgentMap());

		}

		/**
		 * Creates a new agent and adds it to the scenario's environment.
		 */
		@Override
		public void initAgents(MessageLogger logger) {
			if (destinations.size() != 1) {
				logger.log("Error: This agent requires exact one destination.");
				return;
			}
			MapEnvironment env = scenario.getEnv();
			String goal = destinations.get(0);
			MapAgent agent = new MapAgent(env.getMap(), env, search,
					new String[] { goal });
			env.addAgent(agent, scenario.getInitAgentLocation());
		}
	}

	/**
	 * Returns always the heuristic value 0.
	 */
	static class No_Heuristic extends AdaptableHeuristicFunction {

		public double h(Object state) {
			return 0.0;
		}
	}

	/**
	 * A simple heuristic which interprets <code>state</code> and {@link #goal}
	 * as location names and uses the straight-line distance between them as
	 * heuristic value.
	 */
	static class Manhattan_Distance extends AdaptableHeuristicFunction {
		
		public double h(Object state) {
			double result = 0.0;
			Point2D pt1 = map.getPosition((String) state);
			Point2D pt2 = map.getPosition((String) goal);
			if (pt1 != null && pt2 != null) {
				double x = Math.abs(pt2.getX() - pt1.getX());
				double y = Math.abs(pt2.getY() - pt1.getY());
				result = x + y;
			}

			return result;
		}
	}

	static class Euclidean_Distance extends AdaptableHeuristicFunction {

		public double h(Object state) {
			double result = 0.0;
			Point2D pt1 = map.getPosition((String) state);
			Point2D pt2 = map.getPosition((String) goal);
			if (pt1 != null && pt2 != null) {
				double x = Math.abs(pt2.getX() - pt1.getX());
				double y = Math.abs(pt2.getY() - pt1.getY());
				result = Math.sqrt((x * x) + (y * y));
			}
			return result;
		}
	}
	
	static class Diagonal_Distance extends AdaptableHeuristicFunction {

		public double h(Object state) {
			double result = 0.0;
			Point2D pt1 = map.getPosition((String) state);
			Point2D pt2 = map.getPosition((String) goal);
			if (pt1 != null && pt2 != null) {
				float h_diagonal = (float) Math.min(Math.abs(pt1.getX()-pt2.getX()), Math.abs(pt1.getY()-pt2.getY()));
			     float h_straight = (float) (Math.abs(pt1.getX()-pt2.getX()) + Math.abs(pt1.getY()-pt2.getY()));
			     result = (float) (Math.sqrt(2) * h_diagonal + (h_straight - 2*h_diagonal));
			     
			     /**
	                 * Breaking ties: Adding a small value to the heuristic to avoid A* fully searching all equal length paths
	                 * We only want 1 shortest path, not all of them.
	                 * 
	                 * @param p The small value we add to the heuristic. Should be p < (minimum cost of taking one step) / (expected maximum path length) to avoid 
	                 */			     
			     float p = (1/10000);
			     result *= (1.0 + p);
			}
			return result;
		}
	}
}
